2021년 사내 스터디 글 모음
과거 2021년에 사내 스터디를 주최하며 작성했던 글들 모음
[21-07-16 사내모각코] fp-ts monoid (link)
monoid는 semigroup에서 추가로 항등원을 가지고 있는 개념이다.
/***
* Monoid
* semigroup에서 empty가 추가된 개념.
*
* Right identity(오른쪽 항등식): concat(x, empty) = x
* Left identity(왼쪽 항등식): concat(empty, x) = x
* 그렇기 위해서는 항등원이 필요함.
*
* Monoid에서는 항등원(empty)를 넣으면 위의 identity가 지켜진다는 것이 보장되므로,
* 연산에 대한 안전성?이 보장되는 셈
* 그러므로 항등원이 있다면 Monoid로 선언하는 것이 좋다.
* https://www.notion.so/01f55ada5f6548b3a071275a984e03a7#87a3aab453a14c2b9f990acd543f0368
***/
import { concatAll, Monoid } from 'fp-ts/lib/Monoid'
import * as S from 'fp-ts/string'
import * as B from 'fp-ts/boolean'
import { pipe } from 'fp-ts/function'
import * as N from 'fp-ts/number'
import { Semigroup } from 'fp-ts/lib/Semigroup'
import { contramap, getMonoid, reverse } from 'fp-ts/Ord'
import { sort } from 'fp-ts/lib/Array'
const monoidSum: Monoid<number> = {
concat: (x, y) => x + y,
// <number, +>의 항등원 0
empty: 0,
}
const monoidProduct: Monoid<number> = {
concat: (x, y) => x * y,
// <number, *>의 항등원 1
empty: 1,
}
const monoidString: Monoid<string> = {
concat: (x, y) => x + y,
// <string, +>의 항등원 ''
empty: '',
}
const monoidAll: Monoid<boolean> = {
concat: (x, y) => x && y,
// <boolean, &&>의 항등원 true
empty: true,
}
const monoidAny: Monoid<boolean> = {
concat: (x, y) => x || y,
// <boolean, ||>의 항등원 false
empty: false,
}
// concatAll을 할때 semigroup에서 했던 것과는 다르게 초기값을 줄 필요가 없음.
concatAll(monoidSum)([1, 2, 3, 4]) /*?*/
concatAll(monoidProduct)([1, 2, 3, 4]) // 24
concatAll(monoidString)(['a', 'b', 'c']) // 'abc'
concatAll(monoidAll)([true, false, true]) // false
concatAll(monoidAny)([true, false, true]) // true
// 다음과 같은 semigroup은 monoid가 될 수 없다.
const semigroupSpace: Semigroup<string> = {
concat: (x, y) => x + ' ' + y,
}
semigroupSpace.concat('a', '') // 'a '
semigroupSpace.concat('', 'b') // ' b'
monoidString.concat('a', monoidString.empty) // 'a'
monoidString.concat(monoidString.empty, 'b') // 'b'
interface User {
readonly id: number
readonly name: string
readonly age: number
readonly rememberMe: boolean
}
const byName = pipe(
S.Ord,
contramap((p: User) => p.name)
)
const byId = pipe(
N.Ord,
contramap((p: User) => p.id)
)
const byAge = pipe(
N.Ord,
contramap((p: User) => p.age)
)
const byRememberMe = pipe(
B.Ord,
contramap((p: User) => p.rememberMe)
)
const M = getMonoid<User>() /*?*/
const users: Array<User> = [
{ id: 1, name: 'Guido', age: 47, rememberMe: false },
{ id: 2, name: 'Guido', age: 46, rememberMe: true },
{ id: 3, name: 'Giulio', age: 44, rememberMe: false },
{ id: 4, name: 'Giulio', age: 44, rememberMe: true },
]
// 넣는 순서에 따라 1순위 2순위 정렬이 달라짐... 왜?????
// 결합법칙에 위배되지 않나???
// const ord1 = concatAll(M)([byId])
const ord11 = M.concat(byRememberMe, byId)
const ord12 = M.concat(byId, byRememberMe)
// sort(ord1)(users) /*?*/
sort(ord11)(users) /*?*/
/*
* [ { id: 1, name: 'Guido', age: 47, rememberMe: false },
{ id: 3, name: 'Giulio', age: 44, rememberMe: false },
{ id: 2, name: 'Guido', age: 46, rememberMe: true },
{ id: 4, name: 'Giulio', age: 44, rememberMe: true } ]
* */
sort(ord12)(users) /*?*/
/*
* [ { id: 1, name: 'Guido', age: 47, rememberMe: false },
{ id: 2, name: 'Guido', age: 46, rememberMe: true },
{ id: 3, name: 'Giulio', age: 44, rememberMe: false },
{ id: 4, name: 'Giulio', age: 44, rememberMe: true } ]
* */
// sort by name, then by age, then by `rememberMe`
const ord2 = concatAll(M)([byName, byAge, byRememberMe])
sort(ord2)(users) /*?*/
// now `rememberMe = true` first, then by name, then by age
const ord3 = concatAll(M)([reverse(byRememberMe), byName, byAge])
sort(ord3)(users) /*?*/
[21-07-08 개인스터디] fp-ts: semigroup (link)
이론적으로 나에게 가장 도움이 된 글은 이 글이였다.
https://www.johndcook.com/blog/2018/07/08/weak-groups/
export interface Magma<A> {
readonly concat: (x: A, y: A) => A
}
export interface Semigroup<A> extends Magma<A> {}
fp-ts에서 semigroup은 이 Magma의 instance 중 하나이다.
fp-ts에서 semigroup은 닫혀있는(closed) 이항 연산을 정의한다.
- 닫혀있다는 것은
임의의 군(group)G, f, g∈G, 임의의 연산 *라고 했을때 f * g = z에서 z는 z∈G
를 만족시킨다는 것이다. - 예로 들자면 number인 군에 속하는 2, 3와, 임의의 연산 +를 했을때 2 + 3 = 5처럼 5도 number군에 속하는 것처럼 말이다.
fp-ts에서 semigroup은 semigroup이라는 군의 역할보다는 군에 대한 닫혀 있는 이항연산을 정의하는 것에 가깝다. 현대대수학에 나온 semigroup처럼 추상적이고 큰 개념까지 알 필요가 없다.
삼항연산부터는 정의할 의미가 없는 게, A + B + C 같은 연산은 일단 A + B 를 구한 뒤 거기다가 + C 를 하면 되기 때문에 결국엔 이항연산으로 환원된다.
a + b + c + d ... = z 라고 할때
(((a + b) + c) + d) ... 처럼 이항연산만으로 모든 항의 연산을 할 수 있다.
interface Semigroup<A> { readonly concat: (x: A, y: A) => A }
- 결합법칙(Associativity)
concat(x, concat(y, z)) = concat(concat(x, y), z)
- 교환법칙(commutative)
zx = xz
예)
a * (b * c) === (a * b) * c
a + (b + c) === (a + b) + c
나눗셈의 경우 괄호에 따라 결과가 달라지기 때문에 결합법칙을 통과하지 못한다.
/***
* Semigroup (반군, subgroup)
* semigroup은 이미 group(군)안에 속해 있기에
* 재검증을 거치지 않아도 된다는 큰 장점이 있다.
* (여기서의 군은, string, number, Array 등이 있다.)
* 그래서 fp-ts에서도 각 군에서 semigroup에 대한 메소드가 속해있다.
***/
import * as EQ from 'fp-ts/Eq'
import * as Opt from 'fp-ts/Option'
import * as B from 'fp-ts/boolean'
import * as N from 'fp-ts/number'
import * as S from 'fp-ts/string'
import * as FN from 'fp-ts/function'
import * as Semi from 'fp-ts/Semigroup'
interface Point {
readonly x: number
readonly y: number
}
/*** struct ***/
// 구조체 형태의 각각에 속성에 semigroup을 정의
const semiPointSum = Semi.struct<Point>({
x: N.SemigroupSum,
y: N.SemigroupSum,
})
semiPointSum.concat({ x: 3, y: 1 }, { x: 2, y: 2 }) /*?*/ // { x:5, y:3 }
const semiPointMin = Semi.struct<Point>({
x: Semi.min(N.Ord),
y: Semi.min(N.Ord),
})
semiPointMin.concat({ x: 4, y: 12 }, { x: 9, y: 6 }) /*?*/ // { x: 4, y:6 }
interface Vector {
from: Point
to: Point
}
// 이런식으로 확장 가능
const semiVectorSum = Semi.struct<Vector>({
from: semiPointSum,
to: semiPointSum,
})
semiVectorSum.concat(
{
from: { x: 1, y: 5 },
to: { x: 3, y: 10 },
},
{
from: { x: 3, y: 2 },
to: { x: 12, y: 4 },
}
) /*?*/ // Vector { from: { x: 4, y: 7 }, to: { x: 15, y: 14 } }
// struct를 쓰지 않고 정의했을때.
const pointSemi2: Semi.Semigroup<Point> = {
concat(p1, p2) {
return {
x: N.SemigroupSum.concat(p1.x, p2.x),
y: N.SemigroupSum.concat(p1.y, p2.y),
}
},
}
pointSemi2.concat({ x: 3, y: 1 }, { x: 2, y: 2 }) /*?*/ // { x:5, y:3 }
/*** tuple ***/
const tupleSemi = Semi.tuple(S.Semigroup, N.SemigroupSum)
tupleSemi.concat(['a', 3], ['b', 10]) /*?*/ // [('ab', 13)]
// 직접 구현
type Tuple2 = [boolean, boolean]
const semiAllArr: Semi.Semigroup<Tuple2> = {
concat(t1, t2) {
return [
B.SemigroupAll.concat(t1[0], t2[0]),
B.SemigroupAll.concat(t1[1], t2[1]),
]
},
}
semiAllArr.concat([true, true], [false, true]) /*?*/ // [false, true]
/*** constant ***/
// concat에 어떤 값을 넣던 처음 고정시킨 값이 나옴
const constant10Semi = Semi.constant(10) /*?*/
constant10Semi.concat(14, 8) /*?*/ // 10
/*** max, min ***/
const numMaxSemi = Semi.max(N.Ord)
const numMinSemi = Semi.min(N.Ord)
numMaxSemi.concat(5, 12) /*?*/ // 12
numMinSemi.concat(5, 12) /*?*/ // 5
/*** first, last ***/
const firstSemi = Semi.first<number>()
const lastSemi = Semi.last<number>()
firstSemi.concat(5, 10) /*?*/ // 5
lastSemi.concat(5, 10) /*?*/ // 10
/*** reverse ***/
// 두 항의 순서를 바꿈
Semi.reverse(firstSemi).concat(5, 10) /*?*/ // 10
/*** boolean - SemigroupAll, SemigroupAny ***/
B.SemigroupAll.concat(true, true) /*?*/ // true
B.SemigroupAll.concat(true, false) /*?*/ // false
/*** concatAll ***/
// 해당 concat을 배열의 원소에 전부 적용
const semiSumAllStart10 = Semi.concatAll(N.SemigroupSum)(10)
semiSumAllStart10([3, 5, 5, 1, 2, 4]) /*?*/ // 30
const isPositiveX = (p: Point) => p.x >= 0
const isPositiveY = (p: Point) => p.y >= 0
/*** function - getSemigroup ***/
// 1. 이항 연산을 처리할 semigroup을 받음
const semigroupPredicate = FN.getSemigroup(B.SemigroupAll)<Point>()
// 2. 각각의 항에 적용할 함수를 받아서 단항 함수를 만듬
const isPositiveXY = semigroupPredicate.concat(isPositiveX, isPositiveY)
isPositiveXY({ x: 1, y: 1 }) // true
isPositiveXY({ x: 1, y: -1 }) // false
isPositiveXY({ x: -1, y: 1 }) // false
isPositiveXY({ x: -1, y: -1 }) // false
const isXGreaterThenY = (p: Point) => p.x > p.y
// 확장
const isPositiveXYAndXGreaterThenY = semigroupPredicate.concat(
isPositiveXY,
isXGreaterThenY
)
isPositiveXYAndXGreaterThenY({ x: 1, y: 1 }) /*?*/ // false
isPositiveXYAndXGreaterThenY({ x: 2, y: 1 }) /*?*/ // true
ref)
https://book.naver.com/bookdb/book_detail.nhn?bid=15621769
내가 군에 대해 공부했던 것들 정리한 것...
추상대수학
[21-06-29 사내모각코] 컴포넌트 카테고라이징 예시 (link)
import * as React from 'react'
import { Page } from '../stories/Page'
import { PropsWithChildren, ReactElement } from 'react'
// 각 컴포넌트에 의미를 부여하고 추가적인 프로퍼티를 줌으로써
// 각각의 컴포넌트가 특정 분야에 특화되어 사용할 수 있도록 한다.
// 모든 시작은 분류됨으로 부터 시작된다. (제 생각입니다.)
// 밑에 컴포넌트의 구분은 그저 예시일뿐, 활용하기에 따라 무궁무진하게 사용될 수 있습니다.
// 리스트 컴포넌트 안에는 아이템 컴포넌트만 들어갈 수 있게 한다던지...
// 페이지 컴포넌트만 받아서 처리하는 네비게이션을 만든다던지...
// 타입스크립트의 https://www.typescriptlang.org/docs/handbook/2/functions.html#call-signatures 패턴을 활용하였습니다.
interface PageComponent<propsT = any, pathT = string> {
(props: PropsWithChildren<propsT>): ReactElement | null
type: 'Page'
path: pathT
}
interface UiComponent<propsT = any, hooksT = unknown, utilsT = unknown> {
(props: PropsWithChildren<propsT>): ReactElement | null
type: 'UI'
hooks: hooksT
utils: utilsT
}
type MyProps = {
className: string
}
// 직접 구현
// 명확성을 위해 type 나중에 unique symbol 로 교체.
// 최상단에서는 아래와 같이 .type, .path로 넣어줘도 타입스크립트에서 타입에 맞게 정의한 것으로 인식합니다.
const MyPage: PageComponent<MyProps, '/my-page'> = ({
className,
children,
}) => <div className={className}>{children}</div>
MyPage.type = 'Page'
MyPage.path = '/my-page'
Object.freeze(MyPage)
// 컴포넌트를 분류하였으면 아래와 같이 특정 컴포넌트일때 처리하는 함수들을 만들 수 있습니다.
function isPageComponent(component: any): component is PageComponent {
return component.type === 'Page'
}
function isUiComponent(component: any): component is UiComponent {
return component.type === 'UI'
}
isPageComponent(MyPage) /*?*/ // true
isUiComponent(MyPage) /*?*/ // false
// 위와 같이 매번 수동적으로 컴포넌트를 정의하는 것이 불편하므로(타입도 따로 넣어줘야 되고)
// 생성기를 만들었습니다.
// 타입스크립트에서 함수로 감싸면 타입 추론이 가능해지므로 훨신 편해지는 것이 많습니다.
// 컴포넌트 props 타입만 넘길 수 있도록 2차 함수로 작성하였습니다.
function makePageComponent<propsT>(Component: React.FC<propsT>) {
// description function 방식 ts-ignore 안쓰고 어떻게 만드냐...ㅠ_ㅠ
// 함수안에서는 PageComponent.path으로 넣어줘도 타입이 안먹음....
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const PageComponent: PageComponent<propsT> = Component
PageComponent.type = 'Page'
return function <pathT extends string>(path: pathT) {
PageComponent.path = path
Object.freeze(PageComponent)
return PageComponent as PageComponent<propsT, pathT>
}
}
function makeUiComponent<propsT>(Component: React.FC<propsT>) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const UiComponent: UiComponent<propsT> = Component
UiComponent.type = 'UI'
return function <hooksT, utilsT>(props: { hooks?: hooksT; utils?: utilsT }) {
UiComponent.hooks = props.hooks
UiComponent.utils = props.utils
Object.freeze(UiComponent)
return UiComponent as UiComponent<propsT, hooksT, utilsT>
}
}
type LessonProps = {
type: 'student' | 'grade' | 'lesson-class'
}
const LessonPage = makePageComponent<LessonProps>(({ type, children }) => (
<div>
<span className="type">{type}</span>
{children}
</div>
))('/lesson-class')
isPageComponent(LessonPage) /*?*/ // true
type ButtonProps = {
color: React.CSSProperties['color']
}
// utils안에 들어가는 타입도 다 정확히 추론함.
const Button = makeUiComponent<ButtonProps>(({ color, children }) => (
<button color={color}>{children}</button>
))({
utils: {
onDelayButtonRightClick: (timeout: number) => {
// ...code
},
} as const,
})
isUiComponent(Button) /*?*/ // true
const Wrap = () => {
// 타입추론 잘 함
Button.utils.onDelayButtonRightClick(1000)
return (
<div>
<LessonPage type="lesson-class">
<h2>lesson</h2>
<Button color="black">버튼</Button>
</LessonPage>
</div>
)
}
ref)
https://www.typescriptlang.org/docs/handbook/2/functions.html#call-signatures
[21-06-27 개인스터디] fp-ts: Ord (feat: semigroup) (link)
Eq에서 제한된 형태이므로 Eq글을 먼저 보고 오는 것을 추천한다. https://gggururu.tistory.com/82
/***
* Ord
* Order 할때 그 Ord 맞다.
* 비교기를 통해 Ordering(1 | 0 | -1)를 반환한다.
* 나중에 배울 semigroup, monoid와 결합하여 여러 Ord들을 결합하여 1순위, 2순위, ... 정렬을 할 수 있다.
***/
import * as Ord from 'fp-ts/lib/Ord'
import * as EQ from 'fp-ts/Eq'
import * as N from 'fp-ts/number'
import * as S from 'fp-ts/string'
import * as Semi from 'fp-ts/Semigroup'
import { Ordering } from 'fp-ts/Ordering'
const numCompare = (x: number, y: number): Ordering => {
if (x > y) return -1
if (x < y) return 1
return 0
}
// 직접 구현
const numOrdDesc: Ord.Ord<number> = {
equals: (x, y) => x === y,
// ~ equals: (x, y) => numCompare(x, y) === 0,
compare: numCompare,
}
/*** reverse ***/
// Ordering 반전
const numOrdAsc = Ord.reverse(numOrdDesc)
const numArr = [1, 7, 2, 8, 4, 9, 5, 3, 6]
numArr.sort(numOrdDesc.compare) /*?*/ // [9, 8, 7, 6, 5, 4, 3, 2, 1]
numArr.sort(numOrdAsc.compare) /*?*/ // [1, 2, 3, 4, 5, 6, 7, 8, 9]
/***
* fromCompare<T>
* (compare(T, T)) -> Ord<T>
***/
const numOrd2 = Ord.fromCompare(numCompare) // Ord<number>
/***
* equalsDefault<T>
* (compare(T, T)) -> equal(T, T)
***/
const numEq: EQ.Eq<number> = EQ.fromEquals(Ord.equalsDefault(numCompare)) // Eq<number>
/***
* tuple<T1, T2, ...>
* (Ord<T1>, Ord<T2>, ...) -> Ord<[T1, T2, ...]>
***/
const tupleOrd: Ord.Ord<readonly [number, string]> = Ord.tuple(N.Ord, S.Ord)
tupleOrd.compare([1, 'f'], [1, 'a']) /*?*/ // 1
tupleOrd.compare([1, 'f'], [1, 'f']) /*?*/ // 0
tupleOrd.compare([1, 'd'], [1, 'f']) /*?*/ // -1
tupleOrd.compare([1, 'd'], [3, 'a']) /*?*/ // -1
tupleOrd.equals([1, 'd'], [1, 'd']) /*?*/ // true
class User {
constructor(readonly name: string, readonly age: number) {}
}
const 가영 = new User('가영', 14)
const 나영 = new User('나영', 12)
const 다영 = new User('다영', 14)
const 라영 = new User('라영', 17)
const 마영 = new User('라영', 10)
/***
* contramap
* (B -> A) -> (F<A>) -> F<B>
* 아래 User, age(number)로 치환해보자면
* (User -> age) -> Ord<age> -> Ord<User>
***/
const userAgeOrd = Ord.contramap((user: User) => user.age)(N.Ord)
const userNameOrd = Ord.contramap((user: User) => user.name)(S.Ord)
// Utils
/*** min, max ***/
const getYoungUser = Ord.min(userAgeOrd)
const getOldUser = Ord.max(userAgeOrd)
getYoungUser(가영, 나영) /*?*/ // { name: '나영', age: 12 }
getOldUser(가영, 나영) /*?*/ // { name: '가영', age: 14 }
/*** gt, lt, geq(gt+eq), leq(gt+eq) ***/
const isYounger = Ord.lt(userAgeOrd)
const isOlder = Ord.gt(userAgeOrd)
isYounger(나영, 가영) /*?*/ // true
isOlder(나영, 가영) /*?*/ // false
/*** between, clamp ***/
const isAgeBetween나영to가영 = Ord.between(userAgeOrd)(나영, 가영)
const getUserForAgeClamp나영to가영 = Ord.clamp(userAgeOrd)(나영, 가영)
isAgeBetween나영to가영(다영) /*?*/ // true
isAgeBetween나영to가영(라영) /*?*/ // false
// User의 age가 나영 ~ 가영의 사이면 User반환
// 나영보다 작으면 나영
// 가영보다 크면 가영
getUserForAgeClamp나영to가영(마영) /*?*/ // 나영
getUserForAgeClamp나영to가영(다영) /*?*/ // 다영
getUserForAgeClamp나영to가영(라영) /*?*/ // 가영
// const reverseCompare = (ordering: Ordering): Ordering => {
// switch (ordering) {
// case -1:
// return 1
// case 0:
// return 0
// case 1:
// return -1
// }
// }
const userOrdSemi = Ord.getSemigroup<User>()
const myUserOrd = userOrdSemi.concat(userAgeOrd, userNameOrd)
const myUserArr = [가영, 나영, 다영, 라영, 마영].sort(myUserOrd.compare)
myUserArr /*?*/
result
myUserArr
[ User { name: '라영', age: 10 },
User { name: '나영', age: 12 },
User { name: '가영', age: 14 },
User { name: '다영', age: 14 },
User { name: '라영', age: 17 } ]
1순위 나이순, 2순위 name순으로 처럼 Ord를 조합할 수 있다.
[21-06-23 사내모각코] pipeline에서 각 함수의 결과를 측정하여 처리할 수 있게 하는 작업 (link)
pipeline에서의 함수 체이닝시 각각의 결과를 lazy하게 측정하여 해당 결과를 통해 다양한 처리를 할 수 있도록 해봤어요.
어제 말씀드린 카테고리 이론 관련하여 기초적인 내용들 쉽게 작성한 글이 있어서 올려봐요! 지금 보니 제가 설명한게 좀 틀린게 있네욤..
https://evan-moon.github.io/2020/01/27/safety-function-composition/
그리고 제가 한건 그 각 함수가 연결되어 실행하는 로직을 generator함수로 감싸서 lazy하게 실행할 수 있게 하고 밑에 conveyorLogger 같은 lazy한 처리를 관찰하고 처리하는 처리기를 따로 둔 것이 제가 한거에요.
예를 들어 중간에 Except가 리턴되면 처리기가 해당 Except를 확인하여 남은 pipeline을 타지 않고 해당하는 코드를 즉시 리턴하게 해줘요.
(generator에서 반환되는 yield 타입 추론을 어떻게 해야할까요..? 😢)
class Except<T extends string = string> {
constructor(public code: T) {}
}
export const conveyor_belt = <T extends ((a: any) => any | Except)[]>(
fns: [...T]
) => {
return function* (arg: Parameters<T[0]>[0]) {
for (const fn of fns) {
yield fn(arg)
}
}
// no lazy
// return (arg: Parameters<T[0]>[0]) =>
// fns.reduce((output, fn) => fn(output), arg)
}
const myConveyor1 = conveyor_belt([
(str: string) => (str !== '10' ? parseInt(str) : new Except('NO TEN STRING')),
(num: number) =>
num <= 100 ? num * 2 : new Except('MUST NUMBER LESS THEN 100') /*?*/,
(num: number) => [...new Array(10)].map((_, i) => num * i) /*?*/,
])
const myConveyor2 = conveyor_belt([
(a: any) => (typeof a === 'string' ? new Except('string not allowed.') : a),
(a: any) => (typeof a === 'number' ? new Except('number not allowed.') : a),
(a: any) => (typeof a === 'boolean' ? new Except('boolean not allowed.') : a),
])
const conveyorLogger =
<conveyorT extends ReturnType<typeof conveyor_belt>>(con: conveyorT) =>
(arg: any) => {
const iterator = con(arg)
let lastValue: any
while (true) {
const it = iterator.next()
if (it.value instanceof Except) return it.value.code
if (it.done) return lastValue
else {
lastValue = it.value
}
}
}
conveyorLogger(myConveyor1)('5') /*?*/ // [0, 5, 10, 15, 20, 25, 30, 35, 40 ,45]
conveyorLogger(myConveyor1)('10') /*?*/ // "NO TEN STRING"
conveyorLogger(myConveyor1)('102') /*?*/ // "MUST NUMBER LESS THEN 100"
conveyorLogger(myConveyor2)('102') /*?*/ // string not allowed.
conveyorLogger(myConveyor2)(142) /*?*/ // number not allowed.
conveyorLogger(myConveyor2)(true) /*?*/ // boolean not allowed.
conveyorLogger(myConveyor2)(function hihi() {
return 'hihi'
}) /*?*/ // fn hihi
[21-06-18 개인스터디] fp-ts Eq 알아보기 + combinators pattern, contramap (link)
/***
* Eq
* 동등성을 나타냄
* 다음과 같은 원칙을 충족시켜야한다.
* 1. Reflexivity: E.equals(a, a) === true
* 2. Symmetry: E.equals(a, b) === E.equals(b, a)
* 3. Transitivity: if E.equals(a, b) === true and E.equals(b, c) === true, then E.equals(a, c) === true
***/
import * as EQ from 'fp-ts/Eq'
import * as Opt from 'fp-ts/Option'
import * as N from 'fp-ts/number'
import * as S from 'fp-ts/string'
import * as ReadonlyArr from 'fp-ts/ReadonlyArray'
import { pipe } from 'fp-ts/lib/function'
/*** fromEquals(f): Eq ***/
// !!!중요!!!: fromEquals로 만들었을때는 값이 같으면 기본적으로 true
// + 조건식의 값이 같으면 true (equals)
// 에 해당하는 새로운 Eq를 만든다.
const plus5Eq1: EQ.Eq<number> = {
equals(x, y) {
return x + 5 === y
},
}
const plus5EqFromFromEquals = EQ.fromEquals<number>((x, y) => x + 5 === y)
// 차이점에 주목!
plus5Eq1.equals(3, 3) /*?*/ // false
plus5EqFromFromEquals.equals(3, 3) /*?*/ // true
plus5Eq1.equals(3, 8) /*?*/ // true
plus5EqFromFromEquals.equals(3, 8) /*?*/ // true
// Option에서 사용되는 예
const optNumEq = Opt.getEq(N.Eq)
optNumEq.equals(Opt.some(10), Opt.some(1)) /*?*/ // false
optNumEq.equals(Opt.some(10), Opt.none) /*?*/ // false
optNumEq.equals(Opt.none, Opt.none) /*?*/ // true
optNumEq.equals(Opt.some(5), Opt.some(5)) /*?*/ // true
interface Item {
id: number
name: string
}
const eqXPlusOneIsY = EQ.fromEquals<Item>((x, y) => x.id === y.id)
eqXPlusOneIsY.equals({ id: 1, name: '펜' }, { id: 2, name: '펜' }) /*?*/ // false
eqXPlusOneIsY.equals({ id: 1, name: '펜' }, { id: 1, name: '중고 펜' }) /*?*/ // true
/***
* combinator
* A => A
* 기존 형태(A)를 유지하며 새로운 A를 만듬
***/
// Eq combinators example
// Eq<A> => Eq<A[]>
export function getEq<A>(E: EQ.Eq<A>): EQ.Eq<ReadonlyArray<A>> {
return EQ.fromEquals(
(xs, ys) =>
xs.length === ys.length && xs.every((x, i) => E.equals(x, ys[i]))
)
}
// number 비교
export const eqNumber: EQ.Eq<number> = {
equals: (x, y) => x === y,
}
// number[] Eq로 확장
// eqNum: Eq<ReadonlyArray<number>>
export const eqNumbers = getEq(eqNumber)
eqNumbers.equals([1, 4, 2], [1, 4, 2]) /*?*/ // true
eqNumbers.equals([1, 4, 2], [1, 4, 3]) /*?*/ // false
// number[][] Eq로 확장
// eqNumbers: Eq<ReadonlyArray<ReadonlyArray<number>>>
export const eq2dNumbers = getEq(eqNumbers)
eq2dNumbers.equals([[1, 4]], [[1, 4]]) /*?*/ // true
eq2dNumbers.equals([[1, 4]], [[1, 5]]) /*?*/ // false
// number[][][] Eq로 확장
// Eq<ReadonlyArray<ReadonlyArray<ReadonlyArray<number>>>>
export const eq3dNumbers = getEq(eq2dNumbers)
eq3dNumbers.equals([[[1, 4]], [[4, 2]]], [[[1, 4]], [[4, 2]]]) /*?*/ // true
eq3dNumbers.equals([[[1, 4]], [[4, 2]]], [[[1, 4]], [[4, 4]]]) /*?*/ // false
// contravariant (https://hackage.haskell.org/package/contravariant-1.4.1/docs/Data-Functor-Contravariant.html#g:3)
// 대표적인 구현체 메소드로는 contramap이 있다.
// 반 공변적으로 적용
// contramap(B -> A) -> (F<A>) -> F<B>
// contramap은 여기서 아주 친절하게 설명이 되어 있다.
// https://medium.com/@stephaneledorze/the-contravariant-functor-a7ae93e2eae0
// ~ EQ.contramap
export const contramap =
<A, B>(f: (b: B) => A) =>
(E: EQ.Eq<A>): EQ.Eq<B> =>
EQ.fromEquals((x, y) => E.equals(f(x), f(y)))
export interface User {
id: number
name: string
}
// ~ contramap((user: User) => user.id)(N.Eq)
export const eqUser: EQ.Eq<User> = pipe(
N.Eq,
contramap((user: User) => user.id)
)
// EQ에 내장되어 있는 contramap 사용
export const eqUserName: EQ.Eq<User> = pipe(
S.Eq,
EQ.contramap((user: User) => user.name)
)
// ReadonlyArray에 있는 getEq 활용하여 derived
export const eqUsers = ReadonlyArr.getEq(eqUser)
export const eq2dUsers = ReadonlyArr.getEq(eqUsers)
export const eq3dUsers = ReadonlyArr.getEq(eq2dUsers)
reference)
contravariant
https://hackage.haskell.org/package/contravariant-1.4.1/docs/Data-Functor-Contravariant.html
combinators pattern
https://wiki.haskell.org/Combinator
contramap
https://medium.com/@stephaneledorze/the-contravariant-functor-a7ae93e2eae0
[21-06-15 사내모각코] Trait typescript로 구현해보기 (link)
Rust Trait
https://doc.rust-lang.org/rust-by-example/trait.html 이것에 대한 개념 구현(컨셉카 같은 느낌으루다가..)
trait은 공동으로 구현할 것을 추상화 해놓은 것.
해당 trait으로 지정한 프로퍼티는 오직 해당 trait만 알아야 하기 때문에 symbol로 내부적으로 생성하여 symbol로 지정하려는 구조체의 프로퍼티를 작성하려고 하였음.
이렇게 하면 해당 trait은 해당 구조체가 trait을 구현하였는지 알 수 있음.
간단하게 구현하는데는 성공했지만 실제로 사용하려니까 unique symbol 취급을 받지 못하여 해당 심볼로 프로퍼티를 선언할 수 없어서 실패함.
→ 적용하려면 nanoid 같은걸로 해서 유니크한 아이디를 만들어서 해야겠음.
*unique symbol과 symbol을 구분하고 unique symbol이 아니면 프로퍼티를 정의할 수 없게 해놓은 것은, 사용자가 심볼로 프로퍼티를 만들었다가 해당 심볼을 잃어버리게 되면 정의해놓은 프로퍼티에 접근하지 못해서 이런 실수를 방지하고자 막아놓은 것으로 추정됨.
export class Trait<T extends Record<string, any>> {
#keyToSym: = new Map<keyof T, symbol>()
#symToTrait = new Map<symbol, any>()
constructor(traitData: T) {
for (const traitDataKey in traitData) {
const sym = Symbol(traitDataKey)
this.#keyToSym.set(traitDataKey, sym)
this.#symToTrait.set(sym, traitData[traitDataKey])
}
}
get syms() {
return Object.fromEntries(this.#keyToSym) as { [key in keyof T]: symbol }
}
private get traits() {
return Object.fromEntries(
[...this.#keyToSym.entries()].map(([key, sym]) => [
key,
this.#symToTrait.get(sym),
])
) as { [key in keyof T]: T[key] }
}
static merge<T extends Trait<any>[]>(...traits: [...T]) {
return new Trait({
...traits.reduce((obj, trait) => {
return Object.assign(obj, { ...Object.entries(trait.traits) })
}, {}),
})
}
sym(name: keyof T): symbol {
const _sym = this.#keyToSym.get(name)
if (!_sym) throw Error('symbol not found.')
return _sym
}
isImpl(t: any) {
const obj = t.prototype?.constructor ? t.prototype : t
return [...this.#keyToSym.values()].every((sym) => obj[sym])
}
}
// 밑에는 걍 테스트 하던거...
const t = new Trait({
name: 'ss',
}) /*?*/
t.sym('name') /*?*/
t.syms /*?*/
class A {}
t.isImpl(A) /*?*/
const s = Symbol()
class B {
[t.sym('name')]: 'dd'; // ERROR!!
[s]: 'aaa'
} /*?*/
reference)
https://github.com/adobe/ferrum#traits
구현 방식을 참고함.
[21-06-02 사내모각코] fp-ts: Option 알아보기 (link)
/*
* 개념
*
* *** Option (https://gcanti.github.io/fp-ts/modules/Option.ts.html) ***
* Some과 None으로 나뉘어지는 것. 있음과 없음의 개념
* @see https://rinthel.github.io/rust-lang-book-ko/ch06-01-defining-an-enum.html?highlight=Option#option-%EC%97%B4%EA%B1%B0%ED%98%95%EA%B3%BC-null-%EA%B0%92-%EB%B3%B4%EB%8B%A4-%EC%A2%8B%EC%9D%80-%EC%A0%90%EB%93%A4
* 위의 링크를 보면 왜 null, undefined 대신 Option을 써야하는지 잘 알려준다.
* 그리고 여러 iterator, 다른 인터페이스와 상호작용으로 훨신 강력하게 사용할 수 있다.
* */
import * as Opt from 'fp-ts/Option'
/*** Constructor ***/
// type Option<A> = None | Some<A>
// * None의 인스턴스는 none
// * Some의 인스턴스는 some
// * isSome, isNone으로 체크 가능
Opt.some(1) /*?*/
Opt.none /*?*/
/* fromPredicate */
// 조건식에 통과되면 some(통과된 value) 아니면 none인 함수를 반환
const greaterThen10 = Opt.fromPredicate((d: number) => d > 10)
greaterThen10(15) // some(15)
greaterThen10(5) // none
/* ap(opt<A>)((opt<A>) => opt<B>) */
// apply: 1. 값이 바인딩 되어 있고, 2. 그 바인딩된 값을 처리할 함수를 넘김
// 바인딩된 값이 none인 경우 어떤 함수던 none
// some인 경우에만 처리됨
const apN3 = Opt.ap(Opt.some(3))
const apNone = Opt.ap(Opt.none)
apN3(Opt.none) /*?*/
apN3(Opt.some((n) => n * 10)) /*?*/
apN3(Opt.some((n) => (n < 5 ? 0 : 10))) /*?*/ // 0
apNone(Opt.some((v) => 10)) /*?*/ // none
apNone(Opt.some((n) => n * 10)) /*?*/ // none
/*** destructors ***/
/* match(onNone, onSome) */
// none일때와 some일때와 분기 처리.
const matchOption = Opt.match(
() => 'a none',
(a) => `a some containing ${a}`
)
matchOption(Opt.some(10)) /*?*/ // a some containing 10
matchOption(Opt.none) /*?*/ // a none
/* getOrElse(onNone) */
// some이 있으면 some의 값을, none이면 onNone의 리턴 값을 줌
const getOrElseFn = Opt.getOrElse(() => 'hoejun')
getOrElseFn(Opt.some('kang')) /*?*/ // kang
getOrElseFn(Opt.some('im')) /*?*/ // im
getOrElseFn(Opt.none) /*?*/ // hoejun
/*** Guards ***/
Opt.isSome(Opt.some(1)) /*?*/ // => true
Opt.isNone(Opt.none) /*?*/ // => true
/*** interop ***/
/* fromNullable(v) */
// null, undefined => none
// value => some(value)
Opt.fromNullable(undefined) /*?*/ // none
Opt.fromNullable(null) /*?*/ // none
Opt.fromNullable(10) /*?*/ // some(10)
Opt.fromNullable('hi') /*?*/ // some(hi)
/* toNullable, toUndefined */
Opt.toNullable(Opt.none) /*?*/ // null
Opt.toUndefined(Opt.none) /*?*/ // undefined
/* fromNullableK(f) */
// fromNullable의 고차함수 버전
const stringOptionFn = Opt.fromNullableK((v) =>
typeof v === 'string' ? v : null
)
stringOptionFn(10) /*?*/ // none
stringOptionFn('hihi') /*?*/ // some('hihi')
/* tryCatch(f) */
// throw 되면 none, 아니면 반환된 값(v)이 some(v)
Opt.tryCatch(() => {
throw new Error()
}) /*?*/ // none
Opt.tryCatch(() => {
return 10
}) /*?*/ // some(10)
/* tryCatchK(f)(v) */
// tryCatch의 고차함수 버전
const f = Opt.tryCatchK((v: number) => {
if (v < 10) {
throw Error()
}
return v * 2
}) /*?*/
f(5) /*?*/ // none
f(14) /*?*/ // some(28)
/*** instance ***/
// Option => Other Instance
import * as N from 'fp-ts/number'
import { Option } from 'fp-ts/Option'
/* getEq(Eq) */
// Option을 비교하는 Eq(비교기) 인스턴스를 만듬
// 이러면 이제 Eq<Option<number>>가 됨
const numEq = Opt.getEq(N.Eq)
numEq.equals(Opt.some(10), Opt.some(1)) /*?*/ // false
numEq.equals(Opt.some(10), Opt.none) /*?*/ // false
numEq.equals(Opt.some(5), Opt.some(5)) /*?*/ // true
/*** Model ***/
// interface None {
// readonly _tag: 'None'
// }
// interface Some<A> {
// readonly _tag: 'Some'
// readonly value: A
// }
// type Option<A> = None | Some<A>
/*** Utils ***/
/* Do */
// 뭐하는 앤지 모르겠음..
Opt.Do /*?*/ // some({})
/* exists(f)(v) */
// pred를 받은 후 Option을 받아 some안의 value를 검증함
const graterThan1 = Opt.exists((n: number) => n >= 1)
graterThan1(Opt.some(3)) /*?*/ // true
graterThan1(Opt.some(0)) /*?*/ // false
graterThan1(Opt.none) /*?*/ // false